/******************************************************************************/
package jamie.org.sil.www.usxUtils;

import java.io.IOException;
import java.util.Stack;


/******************************************************************************/
/**
   An output manager.
   <p>

   Depending upon the target platform to which you are working and upon the
   configuration options you have selected, you may need different kinds of
   output to go to different destinations.  Thus, for instance, you may wish
   to route the main text to one file, and introductory content to another.
   On the other hand, you may not <i>always</i> want to do this: on another
   run, you may want to route both kinds of output to the same file.  And on
   some runs, you may wish to suppress certain kinds of output altogether.
   <p>

   We recognise five kinds of output here -- the main text, introductory
   material, footnote content, endnote content, and cross-reference content.
   Each of these is capable of being handled separately (although I suspect it
   will be unusual for us ever to use the full flexibility this affords, and at
   the time of writing, we have yet to encounter any situation in which we would
   wish to handle endnotes separately from footnotes -- I suspect such treatment
   would be of use only with printed output).  Each of these is identified by a
   small integer identifier (drawn from the set whose names start
   <i>C_Destination</i>).
   <p>

   Until you indicate where output should go for a given destination,
   the output goes to the bit bucket.  You specify where the output
   should go in one of two ways: you can use {@link #setDestination},
   which currently exists only in a form which sends output to a given
   file; or you can use {@link #linkDestination}, which makes one
   destination point to the same place as another.  If you use
   linkDestination to make destination A point to the same place as
   destination B, note that A picks up the setting for B as it stands
   at the moment you make the call -- it doesn't track any changes
   you subsequently make to the destination for B.
   <p>

   Where you have arranged for A to point to the same place as B,
   we refer to A here as a <i>secondary</i> destination.  Most
   operations (and in particular, writing) on a secondary
   destination have the same effect as if the operation were
   carried out on the associated primary destination.  The one
   exception is <i>closing</i> a secondary destination.  In this
   case, the secondary destination is <i>marked</i> as closed (and
   will therefore not be reported subsequently as open), and its
   link to the primary destination is broken (in fact it is now
   set to point to the bit bucket), but the actual destination
   (ie the associated primary destination) remains open -- primary
   destinations are physically closed only when <i>close</i> is
   called overtly on the primary destination itself.
   <p>

   Note that I make the assumption, where you have a primary
   destination and one or more associated secondary destinations, that
   the primary destination will be closed overtly, and that it will be
   the last thing to be closed.  It doesn't matter which is opened
   first, since whichever is opened first, it will have the same
   effect.
   <p>

   Note that the MyOutputStream class includes methods which write a
   header and / or a trailer to their output.  The header is
   <i>not</i> written at the time you notionally open the output
   stream -- it is written only when you actually send some content to
   the stream.  Similarly the trailer is not written when you
   notionally close the stream, but only if you close it having
   actually written something to it.  This ensures that we do not
   create files comprising just a header and a trailer under
   circumstances where we have no actual data content.  This seems to
   be the behaviour we most often want to see.
   <p>

   The class has specifically been tuned for use in the context of the
   conversion process, where you will be processing a single USX
   element at a time, and USX elements may be nested.  I arrange for
   each element to have associated with it information which says
   whether it should be written to the main text file, to the notes
   file, etc.  The present class maintains a stack of destinations.
   You can write to the current destination (whatever it happens to
   be, based upon the content of the stack), or you can write to
   a specified destination.  There are also functions available to
   push, pop and peek the stack.
   <p>

   Finally, there is the issue of temporary redirection.  This is of
   interest only where, by default, you are sending output to a
   MyOutputStream (which currently I anticipate will always equate to
   a file).  There may be circumstances where temporarily you need the
   output you are sending to that file either to be buffered
   internally (perhaps with a view to generating output out-of-order),
   or to be thrown away altogether.
   <p>
   
   Throwing output away altogether can be achieved in one of two ways.
   In some cases, a particular tag may have been marked as sending its output
   to the bit bucket (this would be in the spreadsheet which I use to generate
   various parts of the code).  In this case, destination for that tag is
   recorded as being the bit bucket, and provided you don't override the
   standard processing, when the tag is encountered, its destination (ie the
   bit bucket) will be pushed on to the output destination stack, and as a
   consequence all output for that tag and for anything within it will be
   thrown away (or it will provided you any contained tags don't override the
   stack setting).
   <p>
   
   The alternative approach to throwing stuff away is to use <i>ignoreData</i>
   in the present class.  This simply turns off all output to the given
   destination, so long as <i>ignoreData</i> remains in effect.  Settings are
   stacked, so if you have had two nested calls indicating that data should be
   ignored, you need two calls indicating that it should not be ignored before
   output is resumed.
   
   As regards the temporary buffering of output, each output stream has its
   own in-memory buffer, and you can swap to send output to that rather than
   to the "actual" output target at any time.  The present class provides a
   whole collection of methods, all with "memory" in their names, to address
   this requirement.
   <p>
   
   Note that some care is necessary to distinguish the various forms of
   buffering, since all output streams are buffered to some extent (which means,
   for instance, that you can always retrieve the last few characters of the
   output and modify it if the need arises to avoid such things as consecutive
   new lines in the output.  However, in addition to this, each output stream
   has one entirely in-memory buffer which can be used to accumulate data which
   may need to be written out out of order.
   <p>

   IMPORTANT: There are quite a lot of public methods in MyOutputStream.
   They have to be public so the present class can access them.  In general,
   however, you should not access them directly -- the present class mediates
   access to most of these methods, and you should normally use them via the
   present class.
 */

public class OutputManager
{
  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                                 Public                                 **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  public static final int C_Destination_Current         = -1;
  public static final int C_Destination_Scripture       = 0;
  public static final int C_Destination_Introduction    = 1;
  public static final int C_Destination_Footnotes       = 2;
  public static final int C_Destination_Endnotes        = 3;
  public static final int C_Destination_CrossReferences = 4;
  public static final int C_Destination_BitBucket       = 5;
  private static final int C_N_Destinations = C_Destination_BitBucket + 1;

  public static final int C_Destination_TBD = C_Destination_BitBucket; // Should never see this.


  /****************************************************************************/
  /**
     Singleton.

     @return Singleton instance.
  */

  public static OutputManager instance ()
  {
    if (null == m_Instance) m_Instance = new OutputManager();
    return m_Instance;
  }


  /****************************************************************************/
  /**
   * Clears any data buffered in the in-memory buffer for the current destination.
   */
  
  public void clearMemoryBufferContents ()
  {
    clearMemoryBufferContents(peekDestination());
  }
  
  
  /****************************************************************************/
  /**
   * Clears any data buffered in the in-memory buffer for the given destination.
   * 
   * @param destination
   */
  
  public void clearMemoryBufferContents (int destination)
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    if (null == m_Destinations[destination] || !m_Destinations[destination].isOpen()) return;
    m_Destinations[destination].clearMemoryBufferContents();
  }
  
  
  /****************************************************************************/
  /**
     Closes the stream.  You may safely call this routine even if the stream
     is already closed -- it simply does nothing in such cases.  If the
     stream is a primary stream, it also closes any associated secondary
     streams.

     @param destination Drawn from the C_Destination set (including
            C_Destination_Current).

     throws IOException
  */

  public void close (int destination) throws IOException
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    
    if (null == m_Destinations[destination] || !m_Destinations[destination].isOpen()) return;
    
    if (m_IsSecondaryDestination[destination])
      m_Destinations[destination] = null;
    else
    {
      for (int i = 0; i < C_N_Destinations; ++i)
        if (m_IsSecondaryDestination[i] && m_Destinations[i] == m_Destinations[destination])
          m_Destinations[i] = null;
            
      m_Destinations[destination].close();
      //m_Destinations[destination] = null;
    }
  }


  /****************************************************************************/
  /**
   * Flushes any data buffered in the in-memory buffer for the current
   * destination to the output stream.
   */
  
  public void flushMemoryBufferToOutputStream () throws IOException
  {
    flushMemoryBufferToOutputStream(peekDestination());
  }
  
  
  /****************************************************************************/
  /**
   * Flushes any data buffered in the in-memory buffer for the given
   * destination to the output stream.
   * 
   * @param destination
   */
  
  public void flushMemoryBufferToOutputStream (int destination) throws IOException
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    if (null == m_Destinations[destination] || !m_Destinations[destination].isOpen()) return;
    m_Destinations[destination].flushMemoryBufferToOutputStream();
  }
  
  
  /****************************************************************************/
  /**
     Obtain the encoding for a given destination.

     @param destination Drawn from the C_Destination set (including
            C_Destination_Current).

     @return Encoding.
  */

  public String getEncoding ()
  {
    return getEncoding(peekDestination());
  }


  /****************************************************************************/
  /**
     Obtain the encoding for a given destination.

     @param destination Drawn from the C_Destination set (including
            C_Destination_Current).

     @return Encoding.
  */

  public String getEncoding (int destination)
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    if (null == m_Destinations[destination] || !m_Destinations[destination].isOpen()) return "";
    return m_Destinations[destination].getEncoding();
  }


  /****************************************************************************/
  /**
     Extracts from the buffer the zero'th matching group based on a given
     pattern.  The extracted string is returned, and is also removed from
     the buffer.  This <i>probably</i> makes sense only when buffering
     to memory, and it is restricted to that use.
     
     @param pattern Pattern to match.  The zero'th matching group is returned,
                    and is also extracted from the buffer.

     @return Buffer contents.
   
     @throws IOException
  */

  public String extractMemoryBufferContents (String pattern) throws IOException
  {
    return extractMemoryBufferContents(peekDestination(), pattern);
  }

          
  /****************************************************************************/
  /**
     Extracts from the buffer the zero'th matching group based on a given
     pattern.  The extracted string is returned, and is also removed from
     the buffer.

     @param destination
     
     @param pattern Pattern to match.  The zero'th matching group is returned,
                    and is also extracted from the buffer.

     @return Buffer contents.
   
     @throws IOException
  */

  public String extractMemoryBufferContents (int destination, String pattern) throws IOException
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    if (null == m_Destinations[destination] || !m_Destinations[destination].isOpen()) return "";
    return m_Destinations[destination].extractMemoryBufferContents(pattern);
  }

          
  /****************************************************************************/
  /**
   * Obtains any data held in the in-memory buffer for the current destination.
   * 
   * @return Buffered data.
   */
  
  public String getMemoryBufferContents () throws IOException
  {
    return getMemoryBufferContents(peekDestination());
  }
  
  
  /****************************************************************************/
  /**
   * Obtains any data held in the in-memory buffer for the given destination.
   * 
   * @param destination
   * 
   * @return Buffered data.
   */
  
  public String getMemoryBufferContents (int destination) throws IOException
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    if (null == m_Destinations[destination] || !m_Destinations[destination].isOpen()) return "";
    return m_Destinations[destination].getMemoryBufferContents();
  }
  
  
  /****************************************************************************/
  /**
   * Returns an indication of whether we are writing to the in-memory buffer
   * for the current output stream.
   * 
   * @return True if we are writing to the in-memory buffer.
   */
  
  public boolean getMemoryBuffering ()
  {
    return getMemoryBuffering(peekDestination());
  }
  
  
  /****************************************************************************/
  /**
   * Returns an indication of whether we are writing to the in-memory buffer
   * for a given output stream.
   * 
   * @return True if we are writing to the in-memory buffer.
   */
  
  public boolean getMemoryBuffering (int destination)
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    if (null == m_Destinations[destination]) return false;
    return m_Destinations[destination].getMemoryBuffering();
  }
  
  
  /****************************************************************************/
  /**
   * Obtains the name (eg pathname) for the current destination.
   * 
   * @return Name.
   */
  
  public String getName ()
  {
    return getName(peekDestination());
  }
  
  
  /****************************************************************************/
  /**
   * Obtains the name (eg pathname) for the given destination.
   * 
   * @param destination
   * 
   * @return Name.
   */
  
  public String getName (int destination)
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    if (null == m_Destinations[destination] || !m_Destinations[destination].isOpen()) return "";
    return m_Destinations[destination].getName();
  }
  
  
  /****************************************************************************/
  /**
     Returns an indication of whether the stream is open.

     @param destination Drawn from the C_Destination set (including
            C_Destination_Current).

     @return True if open.
  */

  public boolean isOpen (int destination)
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    return (null == m_Destinations[destination]) ? false : m_Destinations[destination].isOpen();
  }

  
  /****************************************************************************/
  /**
     Links a given destination to be the same as another destination.
     Note that the "toDestination" must already have been set to
     a genuine destination at the time the method is called.

     @param destinationWhoseValueWeAreOverwriting Drawn from the C_Destination
            set (excluding C_Destination_Current).

     @param destinationWhoseValueWeAreCopying Drawn from the C_Destination set
             (excluding C_Destination_Current).
  */

  public void linkDestination (int destinationWhoseValueWeAreOverwriting,
                               int destinationWhoseValueWeAreCopying)
                              
  {
    m_Destinations[destinationWhoseValueWeAreOverwriting] = m_Destinations[destinationWhoseValueWeAreCopying];
    m_IsSecondaryDestination[destinationWhoseValueWeAreOverwriting] = true;
  }

  
  /****************************************************************************/
  /**
     Writes a newline to the current destination.
  */

  public void newLine () throws IOException
  {
    newLine(peekDestination());
  }


  /****************************************************************************/
  /**
   * Writes a newline to a given destination.
   * 
   * @param Destination.
  */

  public void newLine (int destination) throws IOException
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    if (null != m_Destinations[destination]) m_Destinations[destination].newLine();
  }


  /****************************************************************************/
  /**
   * Peeks at the end of the currently buffered data for the current destination.
   *
   * @param nChars Number of characters (or is that code points?) to peekDestination.
   *
   * @return Peeked data.
   */
  
  public String peekEndOfData (int nChars)
  {
    return peekEndOfData(peekDestination(), nChars);
  }
  
  
  /****************************************************************************/
  /**
   * Peeks at the end of the currently buffered data for the given destination.
   *
   * @param destination
   *
   * @param nChars Number of characters (or is that code points?) to peekDestination.
   *
   * @return Peeked data.
   */
  
  public String peekEndOfData (int destination, int nChars)
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    if (null == m_Destinations[destination] || !m_Destinations[destination].isOpen()) return "";
    return m_Destinations[destination].peekEndOfData(nChars);
  }
  
  
  /****************************************************************************/
  /**
     Peeks the current output destination.

     @return Current destination.
  */

  public int peekDestination ()
  {
    return m_CurrentOutput.empty() ? C_Destination_BitBucket : m_CurrentOutput.peek();
  }

  
  /****************************************************************************/
  /**
     Pops the destination stack.
  */

  public void popDestination ()
  {
    m_CurrentOutput.pop();
  }

  
  /****************************************************************************/
  /**
     Pushes a new destination on to the stack.

     @param destination
  */
  
  final public void pushDestination (int destination)
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    m_CurrentOutput.push(destination);
  }


  /****************************************************************************/
  /**
   * If the current buffer for the current destination ends in s, removes a
   * single copy of s.
   * 
   * @param s String to remove.
   * 
   * @return True if the buffer has been updated.
   */
  
  public boolean removeTrailingStringFromBufferOnce (String s)
  {
    return removeTrailingStringFromBufferOnce(peekDestination(), s);
  }
  
  
  /****************************************************************************/
  /**
   * If the current buffer for the given destination ends in s, removes a
   * single copy of s.
   * 
   * @param s String to remove.
   * 
   * @return True if the buffer has been updated.
   */
  
  public boolean removeTrailingStringFromBufferOnce (int destination, String s)
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    if (null == m_Destinations[destination] || !m_Destinations[destination].isOpen()) return false;
    return m_Destinations[destination].removeTrailingStringFromBufferOnce(s);
  }
  
  
  /****************************************************************************/
  /**
   * If the current buffer for the current destination ends in s, removes all
   * trailing copies of s.
   * 
   * @param s String to remove.
   * 
   * @return True if the buffer has been updated.
   */
  
  public boolean removeTrailingStringFromBufferRepeated (String s)
  {
    return removeTrailingStringFromBufferRepeated(peekDestination(), s);
  }
  
  
  /****************************************************************************/
  /**
   * If the current buffer for the given destination ends in s, removes all
   * trailing copies of s.
   * 
   * @param s String to remove.
   * 
   * @return True if the buffer has been updated.
   */
  
  public boolean removeTrailingStringFromBufferRepeated (int destination, String s)
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    if (null == m_Destinations[destination] || !m_Destinations[destination].isOpen()) return false;
    return m_Destinations[destination].removeTrailingStringFromBufferRepeated(s);
  }
  
  
  /****************************************************************************/
  /**
   * Replace all occurrences of a given pattern with another string in whatever
   * buffer we are currently writing to (current destination).
   * 
   * @param pat Pattern.
   * @param replaceWith
   */
  
  public void replaceAll (String pat, String replaceWith)
  {
    replaceAll(peekDestination(), pat, replaceWith);
  }
  
  
  /****************************************************************************/
  /**
   * Replace all occurrences of a given pattern with another string in whatever
   * buffer we are currently writing to (current destination).
   * 
   * @param destination
   * @param pat Pattern.
   * @param replaceWith
   */
  
  public void replaceAll (int destination, String pat, String replaceWith)
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    if (null == m_Destinations[destination] || !m_Destinations[destination].isOpen()) return;
    m_Destinations[destination].replaceAll(pat, replaceWith);
  }
  
  
  /****************************************************************************/
  /**
     Sets up a given destination, where the target is a file.

     @param destination Drawn from the C_Destination set (excluding
            C_Destination_Current).

     @param filePath This may be null (or an empty string), in which case
            this output will be suppressed.
    
     @param encoding (eg UTF-8).
  */

  public void setDestination (int destination,
                              String filePath,
                              String encoding,
                              OutputStreamHeaderAndTrailerWriter w) throws IOException
  {
    if (null == filePath || 0 == filePath.length())
      m_Destinations[destination] = null;
    else
    {
      m_Destinations[destination] = new MyOutputStreamFile(filePath, encoding, w);
    }

    m_IsSecondaryDestination[destination] = false;
  }

  
  /****************************************************************************/
  /**
     Sets the encoding for the current destination.
   
     @param Encoding -- eg UTF-8.
  */

  public void setEncoding (String encoding)
  {
    setEncoding(peekDestination(), encoding);
  }


  /****************************************************************************/
  /**
     Sets the encoding for a given destination.

     @param destination Drawn from the C_Destination set (including
            C_Destination_Current).
   
     @param Encoding -- eg UTF-8.
  */

  public void setEncoding (int destination, String encoding)
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    if (null != m_Destinations[destination]) m_Destinations[destination].setEncoding(encoding);
  }


  /****************************************************************************/
  /**
   * Sets the current destination to throw away all output for the time being.
   * 
   * @param destination
   * 
   * @param ignoreData True if data is to be thrown away.
   */
  
  public void setIgnoreData (boolean ignoreData)
  {
    setIgnoreData(peekDestination(), ignoreData);
  }
  
  
  /****************************************************************************/
  /**
   * Sets the given destination to throw away all output for the time being.
   * 
   * @param destination
   * 
   * @param ignoreData True if data is to be thrown away.
   */
  
  public void setIgnoreData (int destination, boolean ignoreData)
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    if (null == m_Destinations[destination]) return;
    m_Destinations[destination].setIgnoreData(ignoreData);
  }
  
  
  /****************************************************************************/
  /**
   * Sets the contents of the in-memory buffer for the current destination.
   * 
   * @param s Data to be buffered.
   */
  
  public void setMemoryBufferContents (String s)
  {
    setMemoryBufferContents(peekDestination(), s);
  }
  
  
  /****************************************************************************/
  /**
   * Sets the contents of the in-memory buffer for the given destination.
   * 
   * @param destination
   * 
   * @param s Data to be buffered.
   */
  
  public void setMemoryBufferContents (int destination, String s)
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    if (null == m_Destinations[destination] || !m_Destinations[destination].isOpen()) return;
    m_Destinations[destination].setMemoryBufferContents(s);
  }
  
  
  /****************************************************************************/
  /**
   * Turns on or off in-memory buffering for the current destination.
   * 
   * @param doMemoryBuffering True if data is to be buffered.
   */
  
  public void setMemoryBuffering (boolean doMemoryBuffering) throws IOException
  {
    setMemoryBuffering(peekDestination(), doMemoryBuffering);
  }
  
  
  /****************************************************************************/
  /**
   * Turns on or off in-memory buffering for the given destination.
   * 
   * @param destination
   * 
   * @param doBuffering True if data is to be buffered.
   */
  
  public void setMemoryBuffering (int destination, boolean doBuffering)
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    if (null == m_Destinations[destination]) return;
    m_Destinations[destination].setMemoryBuffering(doBuffering);
  }
  
  
  /****************************************************************************/
  /**
     Writes a string to the current destination.

     @param s
  */

  public void write (String s) throws IOException
  {
    write(peekDestination(), s);
  }


  /****************************************************************************/
  /**
     Writes a string to a given destination.

     @param destination Index into destination table.

     @param s
  */

  public void write (int destination, String s) throws IOException
  {
    if (C_Destination_Current == destination) destination = peekDestination();
    if (null != m_Destinations[destination]) m_Destinations[destination].write(s);
  }




  
  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                                Private                                 **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /**
     Constructor.
  */

  private OutputManager ()
  {
    for (int i = 0; i < m_Destinations.length; ++i)
    {
      m_Destinations[i] = null;
      m_IsSecondaryDestination[i] = false;
    }
  }


  /****************************************************************************/
  private MyOutputStream[] m_Destinations = new MyOutputStream[C_N_Destinations];
  private boolean[] m_IsSecondaryDestination = new boolean[C_N_Destinations];
  private Stack<Integer> m_CurrentOutput = new Stack<Integer>();
  private static OutputManager m_Instance = null;
}
